package gov.tax.rti.ext.bdf2.jdbc;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Column;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.RowMapper;

public class ObjRowMapper<T> implements RowMapper<T>{
	
	private static final Log logger = LogFactory.getLog(ObjRowMapper.class);
	
	private Class<T> iClazz;
	private Class<? extends T> clazz;
	
	public ObjRowMapper() {
		initEntityType();
	}
	
	public ObjRowMapper(Class<T> entityType) {
		this.clazz = this.iClazz = entityType;
	}

	public ObjRowMapper(Class<T> iType, Class<? extends T> entityType) {
		this.iClazz = iType;
		this.clazz = entityType;
	}
	
	@SuppressWarnings("unchecked")
	protected void initEntityType() {
		Class<?> cl = getClass();
		Type superType = cl.getGenericSuperclass();

		if (superType instanceof ParameterizedType) {
			Type[] paramTypes = ((ParameterizedType) superType)
					.getActualTypeArguments();
			int len = paramTypes.length;
			if (len > 0) {
				this.iClazz = (Class<T>) paramTypes[0];
				if (len > 1) {
					this.clazz = (Class<? extends T>) paramTypes[0];
				}else{
					this.clazz = this.iClazz;
				}
			} else {
				logger.warn("Can not determine EntityType for class ["
						+ cl.getSimpleName() + "].");
			}
		} else {
			logger.warn("[" + cl.getSimpleName()
					+ "] is not a parameterized type.");
		}
	}

	private void process(Class<?> entityType,Map<String, PropertyDescriptor> propertyDescriptorMap)
			throws Exception {
		if (entityType != Object.class) {
			process(entityType.getSuperclass(), propertyDescriptorMap);
			Field[] fields = entityType.getDeclaredFields();
			for (Field field : fields) {
				Column col = field.getAnnotation(Column.class);
				try{
					PropertyDescriptor pd = new PropertyDescriptor(field.getName(),
							this.clazz);
					if (null == col) {
						Method getMethod = pd.getReadMethod();
						col = getMethod.getAnnotation(Column.class);
					}
					if (null != col)
						propertyDescriptorMap.put(col.name().toUpperCase(), pd);
				}catch(IntrospectionException e){
					
				}
			}
			Method[] methods = entityType.getDeclaredMethods();
			for (Method method : methods) {
				Column col = method.getAnnotation(Column.class);
				if(col != null){
					String key = col.name().toUpperCase();
					if(!propertyDescriptorMap.containsKey(key)){
						String name = method.getName();
						if(name.startsWith("get")){
							name = name.substring(3,4).toLowerCase() + name.substring(4);
						}else if(name.startsWith("is")){
							name = name.substring(2,3).toLowerCase() + name.substring(3);
						}
						PropertyDescriptor pd = new PropertyDescriptor(name, this.clazz);
						pd.setReadMethod(method);
						propertyDescriptorMap.put(key, pd);
					}
				}
			}
		}
	}

	public T mapRow(ResultSet rs, int rowNum) throws SQLException {
		try {
			ResultSetMetaData rmd = rs.getMetaData();
			T entity = (T) this.clazz.newInstance();
			Map<String, PropertyDescriptor> propertyDescriptorMap = new HashMap<String, PropertyDescriptor>();
			process(this.clazz, propertyDescriptorMap);
			for (int i = 1; i <= rmd.getColumnCount(); i++) {
				String columnName = rmd.getColumnName(i).toUpperCase();
				PropertyDescriptor pd = propertyDescriptorMap.get(columnName);
				if (null != pd) {
					Class<?> propType = pd.getPropertyType();
					Object value = this.processColumn(rs, i, propType);
					if (propType != null && value == null
							&& propType.isPrimitive()) {
						value = primitiveDefaults.get(propType);
					}
					callSetter(entity, pd, value);
				}
			}
			return entity;
		} catch (Exception e) {
			e.printStackTrace();
		}

		return null;
	}

	protected Object processColumn(ResultSet rs, int index, Class<?> propType)
			throws SQLException {

		if (propType.equals(String.class)) {
			return rs.getString(index);

		} else if (propType.equals(Integer.TYPE)
				|| propType.equals(Integer.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if (decimal == null)
				return null;
			return decimal.intValue();

		} else if (propType.equals(Boolean.TYPE)
				|| propType.equals(Boolean.class)) {
			return new Boolean(rs.getBoolean(index));

		} else if (propType.equals(Long.TYPE) || propType.equals(Long.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if (decimal == null)
				return null;
			return decimal.longValue();

		} else if (propType.equals(Double.TYPE)
				|| propType.equals(Double.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if (decimal == null)
				return null;
			return decimal.doubleValue();

		} else if (propType.equals(Float.TYPE) || propType.equals(Float.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if (decimal == null)
				return null;
			return decimal.floatValue();

		} else if (propType.equals(Short.TYPE) || propType.equals(Short.class)) {
			BigDecimal decimal = rs.getBigDecimal(index);
			if (decimal == null)
				return null;
			return decimal.shortValue();

		} else if (propType.equals(Byte.TYPE) || propType.equals(Byte.class)) {
			return new Byte(rs.getByte(index));

		} else if (propType.equals(Timestamp.class)) {
			return rs.getTimestamp(index);
		} else if (propType.equals(Date.class)) {
			return rs.getTimestamp(index);
		} else {
			return rs.getObject(index);
		}

	}

	private void callSetter(Object target, PropertyDescriptor prop, Object value)
			throws SQLException {

		Method setter = prop.getWriteMethod();

		if (setter == null) {
			return;
		}

		Class<?>[] params = setter.getParameterTypes();
		try {
			// convert types for some popular ones
			if (value != null) {
				if (value instanceof java.util.Date) {
					if (params[0].getName().equals("java.sql.Date")) {
						value = new java.sql.Date(
								((java.util.Date) value).getTime());
					} else if (params[0].getName().equals("java.sql.Time")) {
						value = new java.sql.Time(
								((java.util.Date) value).getTime());
					} else if (params[0].getName().equals("java.sql.Timestamp")) {
						value = new java.sql.Timestamp(
								((java.util.Date) value).getTime());
					}
				}
			}

			// Don't call setter if the value object isn't the right type
			if (this.isCompatibleType(value, params[0])) {
				setter.invoke(target, new Object[] { value });
			} else {
				throw new SQLException("Cannot set " + prop.getName()
						+ ": incompatible types.");
			}

		} catch (IllegalArgumentException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());

		} catch (IllegalAccessException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());

		} catch (InvocationTargetException e) {
			throw new SQLException("Cannot set " + prop.getName() + ": "
					+ e.getMessage());
		}
	}

	private boolean isCompatibleType(Object value, Class<?> type) {
		// Do object check first, then primitives
		if (value == null || type.isInstance(value)) {
			return true;

		} else if (type.equals(Integer.TYPE) && Integer.class.isInstance(value)) {
			return true;

		} else if (type.equals(Long.TYPE) && Long.class.isInstance(value)) {
			return true;

		} else if (type.equals(Double.TYPE) && Double.class.isInstance(value)) {
			return true;

		} else if (type.equals(Float.TYPE) && Float.class.isInstance(value)) {
			return true;

		} else if (type.equals(Short.TYPE) && Short.class.isInstance(value)) {
			return true;

		} else if (type.equals(Byte.TYPE) && Byte.class.isInstance(value)) {
			return true;

		} else if (type.equals(Character.TYPE)
				&& Character.class.isInstance(value)) {
			return true;

		} else if (type.equals(Boolean.TYPE) && Boolean.class.isInstance(value)) {
			return true;

		} else {
			return false;
		}

	}

	private static final Map<Object, Object> primitiveDefaults = new HashMap<Object, Object>();

	static {
		primitiveDefaults.put(Integer.TYPE, new Integer(0));
		primitiveDefaults.put(Short.TYPE, new Short((short) 0));
		primitiveDefaults.put(Byte.TYPE, new Byte((byte) 0));
		primitiveDefaults.put(Float.TYPE, new Float(0));
		primitiveDefaults.put(Double.TYPE, new Double(0));
		primitiveDefaults.put(Long.TYPE, new Long(0));
		primitiveDefaults.put(Boolean.TYPE, Boolean.FALSE);
		primitiveDefaults.put(Character.TYPE, new Character('\u0000'));
	}

}
